<?php

namespace app\utils;

use GuzzleHttp\{Client, Pool};
use GuzzleHttp\Psr7\{Request, Response};
use GuzzleHttp\Exception\{GuzzleException, ServerException};

class Http
{
    private Client $client;
    private string $baseUri = 'https://qyapi.weixin.qq.com/cgi-bin/';
    // 默认请求类型
    private array $header = ['Content-type'=>'application/json'];

    public function __construct($data)
    {
        $this->client = new Client([
            'base_uri' => $data['base_uri']??$this->baseUri, // 遵循RFC 3986，参考http://tools.ietf.org/html/rfc3986#section-5.2
            'verify' => $data['verify']??false,             // 访问https时不检测证书，windows和macOS通常没有证书
            'timeout'  => $data['timeout']??30.0,            // 超时时间
        ]);
    }


    /**
     * 同步请求，返回请求结果的数组
     * @param string $method
     * @param string $uri
     * @param array $body
     * @param array $options
     * @return array
     * @throws UtilsException
     */
    public function request(string $method = 'GET', string $uri = '', array $body = [], array $options = []): array
    {
        $method = strtoupper($method);
        // 不同的请求方法、不同的内容，要不同的数据格式，参考：https://guzzle-cn.readthedocs.io/zh_CN/latest/quickstart.html
        $bodyKey = [
            'GET'   =>  'query', // 查询数据，可以直接使用query参数
            'POST'  =>  'json', // 数据上传，可以直接使用json参数
        ];
        if($body){
            $options[$bodyKey[$method]] = $body;//这里构建json
        }

        try {
            $response = $this->client->request($method, $uri, $options);
            if($response->getStatusCode() !== 200){
                throw new UtilsException('请求失败，http返回状态码：'.$response->getStatusCode());
            }
            return json_decode((string)$response->getBody(), true) ? : [];
        } catch (GuzzleException $e) {
            throw new UtilsException($e->getMessage());
        }
    }

    /**
     * 使用pool并发批量请求，返回请求结果是数组。
     * 示例数据 $list = [['key'=>'id', 'method'=>'post', 'body'=>['field'=>'a', 'field'=>'b',]],];
     * @param array $list
     * @param string $uri
     * @param string|null $pk   使用$list中的唯一标识做返回结果的键
     * @param int $concurrency
     * @return array
     */
    public function requestPool(array $list, string $uri, ?string $pk = null, int $concurrency = 5): array
    {
        // 创建请求对象
        $requests = function ($list) use ($uri) {
            foreach ($list as $val){
                // 构建options
                $options = $this->buildOptions($val['header']??[], $val['body']);
                // 这里顺序就是按照循环的顺序，后续取值也根据循环顺序进行，必须全部构建请求
                yield new Request(
                    strtoupper($val['method']??'GET'),
                    $val['uri']??$uri,      // 批量uri通常一样，如果有不一样的或者token之类的变量，可以在这里重新处理
                    $options['header'],
                    $options['body']??null, // body部分数组会json化，可以是字符、资源、stream
                );
            }
        };
        $responseList = [];// 记录请求结果
        $pool = new Pool($this->client, $requests($list), [
            'concurrency' => $concurrency,
            'fulfilled' => function (Response $response, $index) use(&$responseList, $list, $pk) {
                // 请求完成时记录请求结果
                // 没有传值唯一标识时用数字顺序做键
                $key = $pk ? $list[$index][$pk] : $index;
                // 但foreach是根据加入顺序循环，当使用数字键后续使用时，如要按顺序请使用for循环
                $responseList[$key] = json_decode((string)$response->getBody(), true);
            },
            'rejected' => function (ServerException $reason, $index) use($list) {
                // 请求被拒绝时记录日志
                trace('请求失败，http返回状态码：'.$reason->getCode().'。返回消息：'.$reason->getMessage().'。请求体：'.json_encode($list[$index], JSON_UNESCAPED_UNICODE), 'error');
            },
        ]);

        // 启动
        $promise = $pool->promise();
        // 等待完成所有
        $promise->wait();

        return $responseList;// 返回请求完成的结果集
    }

    private function buildOptions(array $header, $body)
    {
        $options = [];
        $options['header'] = array_merge($this->header, $header);// 与默认头合并

        if($body){
            // json类型时，body传值不能是数组
            if($options['header']['Content-type'] == 'application/json' && is_array($body)){
                $body = json_encode($body, JSON_UNESCAPED_UNICODE);
            }
            $options['body'] = $body;
        }

        return $options;
    }

}